<?php

declare(strict_types=1);

namespace RX\Component\DB\EasyRedis;

use RX\Helpers\RandomHelper;

class EasyRedis
{
    protected Connection      $pool;
    protected \Redis          $connection;
    protected bool            $multiOnGoing = false;

    public function __construct($config = null, $poolName = 'default')
    {
        $this->pool = Connection::getInstance($config, $poolName);
    }

    public function __call($name, $arguments)
    {
        if (!$this->multiOnGoing) {
            $this->connection = $this->pool->getConnection();
        }
        try {
            $data = $this->connection->{$name}(...$arguments);
        } catch (\RedisException $e) {
            $this->pool->close(null);
            throw $e;
        }
        if ($this->multiOnGoing) {
            return $this;
        }
        $this->pool->close($this->connection);
        return $data;
    }

    public function brPop($keys, $timeout)
    {
        $data  = [];
        $start = time();
        try {
            $this->connection = $this->pool->getConnection();
            $data             = $this->connection->brPop($keys, $timeout);
        } catch (\RedisException $e) {
            $end = time();
            if ($end - $start < $timeout) {
                $this->pool->close(null);
                throw $e;
            }
        }
        $this->pool->close($this->connection);
        return $data;
    }

    public function blPop($keys, $timeout)
    {
        $data  = [];
        $start = time();
        try {
            $this->connection = $this->pool->getConnection();
            $data             = $this->connection->blPop($keys, $timeout);
        } catch (\RedisException $e) {
            $end = time();
            if ($end - $start < $timeout) {
                $this->pool->close(null);
                throw $e;
            }
        }
        $this->pool->close($this->connection);
        return $data;
    }

    public function subscribe($channels, $callback)
    {
        try {
            $this->connection = $this->pool->getConnection();
            $this->connection->setOption(\Redis::OPT_READ_TIMEOUT, '-1');
            $data = $this->connection->subscribe($channels, $callback);
        } catch (\RedisException $e) {
            $this->pool->close(null);
            throw $e;
        }
        $this->connection->setOption(\Redis::OPT_READ_TIMEOUT, (string)$this->pool->getConfig()['time_out']);
        $this->pool->close($this->connection);
        return $data;
    }

    public function fill()
    {
        $this->pool->fill();
    }

    public function multi($mode = \Redis::MULTI)
    {
        if (!$this->multiOnGoing) {
            try {
                $this->connection = $this->pool->getConnection();
                $this->connection->multi($mode);
            } catch (\RedisException $e) {
                $this->pool->close(null);
                throw $e;
            }
            $this->multiOnGoing = true;
        }
        return $this;
    }

    public function exec()
    {
        if (!$this->multiOnGoing) {
            return;
        }
        try {
            $result = $this->connection->exec();
        } catch (\RedisException $e) {
            $this->multiOnGoing = false;
            $this->pool->close(null);
            throw $e;
        }
        $this->multiOnGoing = false;
        $this->pool->close($this->connection);
        return $result;
    }

    public function discard()
    {
        if (!$this->multiOnGoing) {
            return;
        }
        try {
            $result = $this->connection->discard();
        } catch (\RedisException $e) {
            $this->multiOnGoing = false;
            $this->pool->close(null);
            throw $e;
        }
        $this->multiOnGoing = false;
        $this->pool->close($this->connection);
        return $result;
    }


    public function lock($key, $ttl = 60)
    {
        try {
            $this->connection = $this->pool->getConnection();
            $randNum          = RandomHelper::character(32);
            $result           = $this->connection->set($key, $randNum, ['NX', 'PX' => $ttl * 1000]);
            $this->pool->close($this->connection);
            if ($result) {
                return $randNum;
            }
        } catch (\RedisException $e) {
            $this->pool->close(null);
            throw $e;
        }
        return null;
    }


    public function unlock($key, $randNum)
    {
        $result = null;
        try {
            $lua              = <<<LUA
if redis.call('get',KEYS[1]) == ARGV[1] then 
   return redis.call('del',KEYS[1]) 
else
   return 0 
end
LUA;
            $this->connection = $this->pool->getConnection();
            $result           = $this->connection->eval($lua, [$key, $randNum], 1);
            $this->pool->close($this->connection);
        } catch (\RedisException $e) {
            $this->pool->close(null);
            throw $e;
        }
        return $result;
    }


    public function lockFunction($key, callable $callback, $ttl = 60)
    {
        $random = $this->lock($key, $ttl);
        if ($random === null) {
            return false;
        }
        $result = call_user_func($callback);
        $this->unlock($key, $random);
        return $result;
    }


    public function rateLimit($key, $limitNum = 200, $ttl = 5)
    {
        $result = null;
        try {
            $lua              = <<<SCRIPT
            redis.call('zAdd',KEYS[1],tonumber(ARGV[2]),ARGV[3])
            redis.call('zRemRangeByScore',KEYS[1],0,tonumber(ARGV[2])-tonumber(ARGV[1]))
            redis.call('expire',KEYS[1],tonumber(ARGV[1]))
            local num = redis.call('zCount',KEYS[1],0,tonumber(ARGV[2]))
            if num > tonumber(ARGV[4]) then
                return 1
            else
                return 0
            end
SCRIPT;
            $this->connection = $this->pool->getConnection();
            $score            = time();
            $nonce            = RandomHelper::character(32);
            $result           = $this->connection->eval($lua, [$key, $ttl, $score, $nonce, $limitNum], 1);
            $this->pool->close($this->connection);
        } catch (\RedisException $e) {
            $this->pool->close(null);
            throw $e;
        }
        return $result;
    }
}
